package ru.mirent.sentence.comparer;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import static java.util.Objects.isNull;

/**
 * Нечеткое сравнение слов и предложений согласно рассчету коэффициента Танимото.
 * Пример переписан с C# из <a href="https://habr.com/ru/articles/341148/">статьи</a>.
 */
public class SentenceComparer {
    /**
     * Минимальное количество символов слова
     */
    private final int minWordLength;
    
    /**
     * Размер подстроки при сравнении двух слов (от 1 до {@link #minWordLength})
     */
    private final int subTokenLength;
    
    /**
     * Порог принятия нечеткой эквивалентности всего предложения
     */
    private final double thresholdSentence;
    
    /**
     * Порог принятия нечеткой эквивалентности между двумя словами
     */
    private final double thresholdWord;
    
    public SentenceComparer(int minWordLength, int subTokenLength, double thresholdSentence, double thresholdWord) {
        if (subTokenLength <= 0) {
            throw new RuntimeException("Длинна подстроки не может быть менее или равной 0");
        }

        if (minWordLength <= 0) {
            throw new RuntimeException("Минимальная длинна слова не может быть менее или равной 0");
        }

        if (subTokenLength > minWordLength) {
            throw new RuntimeException("Длинна подстроки не может быть более минимальной длинны слова");
        }

        this.minWordLength = minWordLength;
        this.subTokenLength = subTokenLength;
        this.thresholdSentence = thresholdSentence;
        this.thresholdWord = thresholdWord;
    }

    public SentenceComparer() {
        this(3, 2, 0.25, 0.45);
    }

    /**
     * Возвращает результат нечеткого сравнения предложений.
     * @param first первое предложение
     * @param second второе предложение
     * @return true в результате совпадения, иначе false
     */
    public boolean isFuzzyEqual(String first, String second) {
        double d = calculateFuzzyEqualValue(first, second);
        System.out.println(d);
        return d >= thresholdSentence;
    }

    /**
     * Вычисляет значение нечеткого сравнения предложений.
     * @param first первое предложение
     * @param second второе предложение
     * @return результат нечеткого сравнения предложений
     */
    public double calculateFuzzyEqualValue(String first, String second) {
        if (isNullOrWhiteSpace(first) && isNullOrWhiteSpace(second)) {
            return 1.0;
        }

        if (isNullOrWhiteSpace(first) || isNullOrWhiteSpace(second)) {
            return 0.0;
        }

        String normalizedFirst = normalizeSentence(first);
        String normalizedSecond = normalizeSentence(second);

        List<String> tokensFirst = getTokens(normalizedFirst, minWordLength);
        List<String> tokensSecond = getTokens(normalizedSecond, minWordLength);

        List<String> fuzzyEqualsTokens = getFuzzyEqualsTokens(tokensFirst, tokensSecond);

        int equalsCount = fuzzyEqualsTokens.size();
        int firstCount = tokensFirst.size();
        int secondCount = tokensSecond.size();

        return (1.0 * equalsCount) / (firstCount + secondCount - equalsCount);
    }

    /**
     * Нормализация предложений: перевод текста в нижний регистр, удаление не букв и не цифр
     * @param sentence предложение
     * @return нормализованное предложение
     */
    public String normalizeSentence(String sentence) {
        String lowerText = sentence.toLowerCase(Locale.ROOT);
        StringBuilder result = new StringBuilder();

        for (int i = 0; i < lowerText.length(); i++) {
            char c = lowerText.charAt(i);
            if (isNormalChar(c)) {
                result.append(c);
            }
        }

        return result.toString();
    }

    /**
     * Проверить, что символ равен букве, цифре или пробелу.
     * @param value проверяемый символ
     * @return true - если символ буква или цифра или пробел, иначе - false
     */
    public boolean isNormalChar(char value) {
        return Character.isLetterOrDigit(value) || value == ' ';
    }

    /**
     * Разбивка предложения на слова.
     * @param sentence исходное предложение
     * @param minWordLength минимальное количество символов для слова
     * @return список слов
     */
    public List<String> getTokens(String sentence, int minWordLength) {
        List<String> words = List.of(sentence.split(" "));
        List<String> result = new ArrayList<>();

        for (String word : words) {
            if (word.length() >= minWordLength) {
                result.add(word);
            }
        }

        return result;
    }

    /**
     * Проверка строки на null, пустоту и наличие пробелов
     * @param text текст для проверки
     * @return true - строка равна null, строка пустая или содержит пробелы, иначе false
     */
    public boolean isNullOrWhiteSpace(String text) {
        if (isNull(text)) {
            return true;
        } else if (text.isEmpty()) {
            return true;
        } else return text.trim().isEmpty();
    }

    /**
     * Возвращает результат нечеткого сравнения слов.
     * @param firstToken первое слово
     * @param secondToken второе слово
     * @return результат нечеткого сравения слов
     */
    public boolean isTokensFuzzyEqual(String firstToken, String secondToken) {
        int equalSubtokensCount = 0;
        boolean[] usedTokens = new boolean[secondToken.length() - subTokenLength + 1];

        for (int i = 0; i < firstToken.length() - subTokenLength + 1; i++) {
            String subtokenFirst = firstToken.substring(i, i + subTokenLength);
            for (int j = 0; j < secondToken.length() - subTokenLength + 1; j++) {
                if (!usedTokens[j]) {
                    String subtokenSecond = secondToken.substring(j, j + subTokenLength);

                    if (subtokenFirst.equals(subtokenSecond)) {
                        equalSubtokensCount++;
                        usedTokens[j] = true;
                        break;
                    }
                }
            }
        }

        int subtokenFirstCount = firstToken.length() - subTokenLength + 1;
        int subtokenSecondCount = secondToken.length() - subTokenLength + 1;

        double tanimoto = (1.0 * equalSubtokensCount) / (subtokenFirstCount + subtokenSecondCount - equalSubtokensCount);

        return tanimoto >= thresholdWord;
    }

    /**
     * Возвращает эквивалентные слова из двух наборов.
     * @param tokensFirst слова из первого предложения
     * @param tokensSecond слова из второго набора предложений
     * @return набор эквивалентных слов
     */
    public List<String> getFuzzyEqualsTokens(List<String> tokensFirst, List<String> tokensSecond) {
        List<String> equalsTokens = new ArrayList<>();
        boolean[] usedTokens = new boolean[tokensSecond.size()];

        for (int i = 0; i < tokensFirst.size(); i++) {
            for (int j = 0; j < tokensSecond.size(); j++) {
                if (!usedTokens[j]) {
                    if (isTokensFuzzyEqual(tokensFirst.get(i), tokensSecond.get(j))) {
                        equalsTokens.add(tokensFirst.get(i));
                        usedTokens[j] = true;
                        break;
                    }
                }
            }
        }

        return equalsTokens;
    }
}
